[GSoC 2019] RabbitMQ architecture for notification system #382
Conversation
Thanks @aquibbaig for reducing the scope of this PR. It looks mostly good, but let's start in even smaller steps by only doing the RabbitMQ architectural stuff for now.
|
|
It's fine indeed, no idea what problem github has here. The comment about removing all bug the rabbitmq commit still holds, however. |
a9ffb3b
to
6fb1420
Compare
@imphil just force pushed the branch reflecting required changes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @aquibbaig for the update to this PR. I've left a couple comments inline.
On the commit structure:
- Please modify the commits to have a single commit where you add all the rabbitmq configuration/classes, and the Notification(Hydrator) class. So that's essentially combining the two commits you now have into one.
- Separate the demo use case of adding a message to the queue from the remaining infrastructure. so the changes to ProjectController.php should be a separate commit.
The composer.lock file changes should not be part of this PR at all.
@@ -19,6 +19,9 @@ old_sound_rabbit_mq: | |||
update_project_info: | |||
connection: default | |||
exchange_options: {name: 'update-project-info', type: fanout} | |||
send_notification: | |||
connection: default | |||
exchange_options: {name: 'send-notification', type: fanout} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about notification
instead of send-notification
?
web_notification: | ||
connection: default | ||
exchange_options: {name: 'send-notification', type: fanout} | ||
queue_options: {name: 'send-notification'} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a setup where we have one queue per consumer, as in this picture:
In the tutorial at https://www.rabbitmq.com/tutorials/tutorial-three-python.html they're using an empty queue name to generate two unique queue names. You're using the same queue name for both consumers. Please double-check that that leads to the right queue structure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The message currently goes through the following areas
- An Exchange first
- Queue next
- Both the consumers where
shouldHandle()
checks if it will be handled by the one it is in
But I guess we want a structure where it goes through
- An Exchange first
- The Queue of the specific Consumer (two queues now)
- To the specific Consumer where a
shouldHandle()
will confirm that it is handled indeed by this Consumer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll be working on this today itself
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@imphil @agathver
So, I have come up with two promising architecture designs. I believe we need very small attention to detail here. I found there is another way here(it may be unnecessary).
A Direct Exchange architecture
Advantages
- Less message load in the buffer
A fanout Exchange architecture
Advantages
- No routing key required as it broadcasts messages to both queues, simpler
- No logic required beforehand to generate a routing key
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to go for the "fanout" architecture as inserting a message should be fast (it's in the synchronous page rendering path in many cases).
connection: default | ||
exchange_options: {name: 'send-notification', type: fanout} | ||
queue_options: {name: 'send-notification'} | ||
callback: email_notification_consumer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that callback the default and can be omitted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checked that it expects a callback
to be there else an error is thrown
site/app/config/services.yml
Outdated
@@ -94,3 +97,6 @@ services: | |||
App\RepoCrawler\GitRepoCrawler: | |||
tags: | |||
- 'app.repo_crawler' | |||
|
|||
# Register Mgilet NotificationManager as a service to be used in WebNotificationConsumer | |||
Mgilet\NotificationBundle\Manager\NotificationManager: '@mgilet.notification' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed this before: This commit is titled " setup Rabbit MQ architecture with respective consumers ". the mgilet bundle is not needed here, and should not appear anywhere in this commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a mistake I overlooked accidentally
* AbstractNotificationConsumer constructor. | ||
* @param UserManagerInterface $userManager | ||
*/ | ||
public function __construct(UserManagerInterface $userManager) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the UserManagerInterface part of this interface?
* | ||
* Class NotificationHydrator | ||
*/ | ||
class NotificationHydrator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NotificationHydrator is a strange name. This is just a plain old data class (POD), so how about Notification
?
/** | ||
* @var string $link | ||
*/ | ||
protected $link; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's that for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, Mgilet provides us with a link
field by default which is nullable. So, we can either add/remove this field, later on, based on our usage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So let's do it the other way around: remove the link field in our Notification(Hydrator)
class, as I don't see any use for it. If mgilet uses it that's fine, we can always set it to null.
/** | ||
* @var int $userIdentifier | ||
*/ | ||
protected $userIdentifier; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the sender or the recipient? And it should be of type User
; you can serialize that into a user id when sending.
* @var int $userIdentifier | ||
*/ | ||
protected $userIdentifier; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class is missing a bit of meta data: send timestamp, author
* | ||
* @author Aquib Baig <aquibbaig97@gmail.com> | ||
*/ | ||
class NotificationProducerService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class seems to be modeled after QueueDispatcherService, which isn't actually a particularly good design and needs to be fixed now that we have more than one producer service.
Please remove this class, you can inject the auto-generated service (i.e. the one you'd pass here as $notificationProducer) directly into the class method/class that needs to produce messages. (ProjectController.php in this case). Please don't use the ProducerInterface
, but the actual class -- all producers conform to this interface, and autowire cannot really figure out which service class is the right one to bind now that there are two options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, while publishing a message, do we need to get the producer just as
this->get('@old_sound_rabbit_mq.notification_producer')->publish($msg)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can inject the NotificationProducerService instead of using $this->get()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's better
cb7c179
to
63a3fca
Compare
@imphil Just made the changes as discussed in yesterday's meeting. Added a few comments too, sorry for the mess the last time. I hope it's fine now. If not, please let me know |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @aquibbaig. Looks pretty good now, some small fixes and we're there.
public function execute(AMQPMessage $msg) | ||
{ | ||
try { | ||
$notification = unserialize($msg->body); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can make $notification a member variable ($this->notification), and remove the argument from shouldHandle and handle()
try { | ||
$notification = unserialize($msg->body); | ||
|
||
return $this->handle($notification); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to call shouldHandle() as well.
* | ||
* @return bool | ||
*/ | ||
abstract protected function shouldHandle(Notification $notification): bool; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can always return true in the abstract implementation, it's a sane default.
use Psr\Log\LoggerInterface; | ||
|
||
/** | ||
* Class EmailNotificationConsumer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this summary line more descriptive, e.g. "Send out notifications over email"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That comment needs resolving in a updated push.
*/ | ||
protected function handle(Notification $notification) | ||
{ | ||
if ($this->shouldHandle($notification)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should go into the execute() function (see the comment there)
site/src/Util/Notification.php
Outdated
* | ||
* @var User $user | ||
*/ | ||
protected $user; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
naming: user -> recipient
site/src/Util/Notification.php
Outdated
* | ||
* @var string author | ||
*/ | ||
protected $author; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, thinking about it, in almost all cases our messages come from the "system", so there's actually no sender/author. You can remove this field.
site/src/Util/Notification.php
Outdated
|
||
/** | ||
* Notification constructor. | ||
* @throws \Exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://php.net/manual/en/datetime.construct.php
It emits an exception in case of error, if it contains an invalid format, but we sure don't need to throw it. As we do not set datetimes explicitely
* @param LoggerInterface $logger | ||
* @param Notification $notification | ||
*/ | ||
public function __construct(LoggerInterface $logger, Notification $notification) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove Notification from the constructor, it's not injected or passed there somehow: it's set in the execute() function.
class EmailNotificationConsumer extends AbstractNotificationConsumer | ||
{ | ||
/** | ||
* EmailNotificationConsumer constructor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a empty line after this one.
use Psr\Log\LoggerInterface; | ||
|
||
/** | ||
* Class EmailNotificationConsumer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That comment needs resolving in a updated push.
site/src/Util/Notification.php
Outdated
protected $message; | ||
|
||
/** | ||
* User associated with the Notification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change this comment to match the variable (it's the recipient of the notification)
site/src/Util/Notification.php
Outdated
protected $subject; | ||
|
||
/** | ||
* Actual definition of the Notification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actual definition -> Notification message
site/src/Util/Notification.php
Outdated
} | ||
|
||
/** | ||
* Returns User associated with a Notification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Get the recipient of this notification"
site/src/Util/Notification.php
Outdated
} | ||
|
||
/** | ||
* Used to bind users to any Notification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Set the recipient of this message"
site/src/Util/Notification.php
Outdated
} | ||
|
||
/** | ||
* Gets the timestamp of a Notification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Get the creation date of this notification"
site/src/Util/Notification.php
Outdated
} | ||
|
||
/** | ||
* Sets the timestamp of a Notification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do something equivalent to the get... description for this field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@imphil Yes. Just did that
Currently creates two consumers for email and web notifications
Thanks @aquibbaig, this now looks good! Merged & ready for new things! |
Aimed at fixing #373 #374 currently. This will be a WIP for now.